import javax.sound.midi.*;

/**
 * Represents a musical pitch, which has
 * a note from the musical alphabet with accidental(s),
 * an octave number, and an associated frequency in Hertz
 * 
 * @author jo
 */
public class Pitch implements Comparable<Pitch>
{
	public static final int ORDINAL_MIDI_OFFSET = 21;
	
	enum Frequency
	{
		A0,Bb0,B0, // [0,2]
		C1,Db1,D1,Eb1,E1,F1,Gb1,G1,Ab1,A1,Bb1,B1, // [3,14]
		C2,Db2,D2,Eb2,E2,F2,Gb2,G2,Ab2,A2,Bb2,B2, // [15,26]
		C3,Db3,D3,Eb3,E3,F3,Gb3,G3,Ab3,A3,Bb3,B3, // [27,38]
		C4,Db4,D4,Eb4,E4,F4,Gb4,G4,Ab4,A4,Bb4,B4, // [39,50]
		C5,Db5,D5,Eb5,E5,F5,Gb5,G5,Ab5,A5,Bb5,B5, // [51,62]
		C6,Db6,D6,Eb6,E6,F6,Gb6,G6,Ab6,A6,Bb6,B6, // [63,74]
		C7,Db7,D7,Eb7,E7,F7,Gb7,G7,Ab7,A7,Bb7,B7, // [75,86]
		C8; // [87]
		
		/**
		 * 
		 * @return frequency in Hz
		 */
		public double frequency()
		{
			return 440 * Math.pow(2,(ordinal() - A4.ordinal())/12.0);
		}
		
		/**
		 * 
		 * @return half step down
		 */
		public Frequency prev()
		{
			return ordinal() == 0 ? this : values()[ordinal()-1];
		}
		
		/**
		 * 
		 * @return half step up
		 */
		public Frequency next()
		{
			return ordinal() == values().length-1 ? this : values()[ordinal()+1];
		}
		
		public Frequency octaveDown()
		{
			return values()[ordinal() - 12];
		}
		
		public Frequency octaveUp()
		{
			return values()[ordinal() + 12];
		}
	}
	
	/**
	 * Must be one of:
	 * A,B,C,D,E,F,G
	 */
	public char note;
	
	/**
	 * Must be one of:
	 * b (flat)
	 * n (natural)
	 * # (sharp)
	 * x (double sharp)
	 */
	public char accidental;
	
	/**
	 * How many of accidental (double flats, etc.).
	 * Must be >= 0
	 */
	public int count;
	
	/**
	 * Must be in the range [0,8]
	 */
	public int octave;
	
	public Frequency freq;
	
	/**
	 * Creates a pitch represented by the string parameter.
	 * Must have the format (without any whitespace):
	 * [A-G][b#x]*[0-8]
	 * 
	 * @param pitch as string input
	 * @throws SyntaxErrorException on bad input
	 */
	public Pitch(String pitch) throws SyntaxErrorException
	{
		if (pitch == null || pitch.length() <= 1)
			throw new SyntaxErrorException("Pitch declaration too short");
		
		char[] pitchar = pitch.toCharArray(); // pitch U char - get it? ;)
		
		if (!validNote(pitchar[0]))
			throw new SyntaxErrorException("No such note (A-G only)");
		note = pitchar[0];
		
		for (int i=1; i < pitchar.length; i++)
		{
			if (validSymbol(pitchar[i])) // accidental
			{
				switch (pitchar[i])
				{
				case 'b':
					if (count > 0 && accidental != 'b')
						throw new SyntaxErrorException("Invalid accidentals");
					accidental = 'b';
					count++;
					break;
					
				case '#':
					if (count > 0 && accidental != '#')
						throw new SyntaxErrorException("Invalid accidentals");
					accidental = '#';
					count++;
					break;
					
				case 'x':
					if (count > 0 && accidental != 'x')
						throw new SyntaxErrorException("Invalid accidentals");
					if (count > 1)
						throw new SyntaxErrorException("Only one double sharp allowed");
					accidental = 'x';
					count++;
					break;
				}		
			}
			else if (Character.isDigit(pitchar[i]))
			{
				if (i != pitchar.length - 1 || pitchar[i] == '9')
					throw new SyntaxErrorException("Invalid octave (0-8 only)");
				octave = pitchar[i] - 48; // ASCII to integer in [0,8]
			}
			else throw new SyntaxErrorException("Unrecognized notation/symbol(s)");
		}
		
		if (count == 0) accidental = 'n';
		
		// get corresponding frequency
		int f = octave;
		
		// edge cases: A0, Bb0, B0
		if (f == 0 && note == 'B') f = 2;
		else f = (f-1) * 12 + 3;
		
		// adjust by note
		switch (note)
		{
		case 'C': break;
		case 'D': f += 2; break;
		case 'E': f += 4; break;
		case 'F': f += 5; break;
		case 'G': f += 7; break;
		case 'A': f += 9; break;
		case 'B': f += 11; break;
		}
		
		freq = Frequency.values()[f];
		
		// apply accidental(s)
		if (accidental != 'n')
		{
			switch (accidental)
			{
			case 'b':
				for (int i=0; i < count; i++)
					freq = freq.prev();
				break;
				
			case '#':
				for (int i=0; i < count; i++)
					freq = freq.next();
				break;
				
			case 'x': // should not be more than 1
				freq = freq.next().next();
				break;
			}
		}
	}
	
	/**
	 * Creates a pitch from a predefined enumerated frequency
	 * 
	 * @param freq as enumeration
	 */
	public Pitch(Frequency freq)
	{
		note = freq.toString().charAt(0);
		
		if (freq.toString().contains("b"))
		{
			accidental = 'b';
			count = 1;
		}
		else accidental = 'n';
		
		octave = freq.toString().charAt(freq.toString().length()-1) - 48;
		
		this.freq = freq;
	}
	
	private boolean validNote(char c)
	{
		char[] validNotes = new char[] {'A','B','C','D','E','F','G'};
		boolean result = false;
		
		for (int i=0; i < validNotes.length; i++)
			if (c == validNotes[i]) return true;
		
		return result;
	}
	
	private boolean validSymbol(char c)
	{
		return c == 'b' || c == '#' || c == 'x';
	}
	
	public int compareTo(Pitch o)
	{
		return freq.ordinal() - o.freq.ordinal();
	}
	
	/**
	 * @return String representation of this pitch with all accidental(s)
	 */
	public String toString()
	{
		StringBuilder result = new StringBuilder();
		
		result.append(note);
		
		for (int i=0; i < count; i++)
			result.append(accidental);
		
		result.append(octave);
		
		return result.toString();
	}
	
	/**
	 * Play this pitch via MIDI
	 * 
	 * @param ms number of milliseconds to hold note
	 * @throws MidiUnavailableException if system does not support MIDI
	 */
	public void play(long ms) throws MidiUnavailableException
	{
		Synthesizer synth = MidiSystem.getSynthesizer();
		synth.open();
		
		MidiChannel channel = synth.getChannels()[0];
		
		// probably acoustic piano on most systems
		channel.programChange(46);
		
		// main volume (cc=7) = max (127)
		channel.controlChange(7,127);
		
		channel.noteOn(freq.ordinal()+ORDINAL_MIDI_OFFSET, 90);
		try
		{
			Thread.sleep(ms);
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		
		channel.allNotesOff();
		synth.close();
	}
	
	/**
	 * test run a melismatic glissando!
	 * @throws MidiUnavailableException if system does not support MIDI
	 * @throws InvalidMidiDataException 
	 */
	public static void melisma() throws MidiUnavailableException, InvalidMidiDataException
	{
		// make the sequence
		Sequence gliss = new Sequence(Sequence.PPQ, 24);
		Track track = gliss.createTrack();
		
		// glissando all 88 keys
		for (int i=21; i <= 108; i++)
		{
			ShortMessage on = new ShortMessage();
			on.setMessage(ShortMessage.NOTE_ON, 0, i, 90);
			
			ShortMessage off = new ShortMessage();
			off.setMessage(ShortMessage.NOTE_OFF, 0, i, 0);
			
			track.add(new MidiEvent(on,3*(i-21)));
			track.add(new MidiEvent(off,3*(i-20)));
			
			// dummy event to prevent clipping
			if (i == 108)
				track.add(new MidiEvent(off,4*(i-19)));
		}
		
		Sequencer seq = MidiSystem.getSequencer();
		seq.setTempoInBPM(120);
		seq.open();
		
		seq.setSequence(gliss);
		
		seq.start();
		try
		{
			Thread.sleep((long)(seq.getMicrosecondLength()/1e3));
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}
		
		seq.close();
	}
	
	public static void main(String[] args) throws SyntaxErrorException, MidiUnavailableException, InvalidMidiDataException
	{
		/*
		
		//Pitch p = new Pitch("C4");
		//p.play(1000);
		
		Pitch.Frequency[] notes = Pitch.Frequency.values();
		
		for (Pitch.Frequency e : notes)
			System.out.print(e + " ");
		
		// Pitch.melisma();
		
		*/
		
		
	}
}
